import {
  Request,
  Body,
  Controller,
  Get,
  Param,
  Post,
  Response,
  UseInterceptors,
} from "@nestjs/common";
import { FastifyReply, FastifyRequest } from "fastify";
import { WebResult } from "src/common/web.result";

import * as fs from "node:fs";
import { md5 } from "js-md5";
import { ImageBase64DTO } from "./dto/image-base64.dto";
import { LoginInterceptor } from "src/user/interceptor/login.interceptor";
import { FileSystemStoredFile, FormDataRequest } from "nestjs-form-data";
import { ImageDTO } from "./dto/image.dto";
import { Throttle } from "@nestjs/throttler";
import {
  ResourceDTO,
  ResourceChunkCodeDTO,
  ResourceChunkDTO,
  ResourceChunkEndDTO,
  MAX_FILE_SIZE,
  MAX_CHUNK_SIZE,
} from "./dto/resource.dto";
import { ResourceService, ResourceType } from "./resource.service";
import { UserTokenService } from "src/user/token.service";
import { PrismaClient } from "@prisma/client";
import configuration from "src/common/configuration";

const CHUNK_CACHE_PATH = `${configuration.resource.rootDirectoryPath}/chunk`;
fs.mkdirSync(CHUNK_CACHE_PATH, { recursive: true });

@Controller("resource")
export class ResourceController {
  constructor(
    private readonly prisma: PrismaClient,
    private readonly userTokenService: UserTokenService,
    private readonly resourceService: ResourceService,
  ) {}

  @Get(["/max/size/file", "/max_size/file", "/max-size/file", "/maxSize/file"])
  getMaxFileSize() {
    return WebResult.success(MAX_FILE_SIZE);
  }
  @Get(["/max/size/chunk", "/max_size/chunk", "/max-size/chunk", "/maxSize/chunk"])
  getMaxChunkSize() {
    return WebResult.success(MAX_CHUNK_SIZE);
  }

  // 上传
  @UseInterceptors(LoginInterceptor)
  @Post(["/", "/upload"])
  @FormDataRequest()
  async uploadResource(
    @Request() request: FastifyRequest,
    @Body() resourceDTO: ResourceDTO,
  ) {
    const uid = this.userTokenService.getUserIDFromRequest(request);
    const file = resourceDTO.file;

    const ext = file.extension;
    let fileName = resourceDTO.name;
    if (!resourceDTO.name.endsWith(ext)) {
      fileName += "." + ext;
    }

    const resourceType = this.resourceService.getResourceTypeByMIMEType(
      file.mimeType,
    );
    const filePath = this.resourceService.getResourceSavePath(
      uid,
      fileName,
      resourceType,
    );
    if (fs.existsSync(filePath)) {
      return WebResult.failure(undefined, "已存在同名文件");
    }
    try {
      fs.writeFileSync(filePath, file.buffer);
      const resourcePublic = (function() {
        if (!resourceDTO.public) {
          return false;
        }
        if (typeof resourceDTO.public === "boolean") {
          return resourceDTO.public;
        }
        const publicString = resourceDTO.public.toLowerCase()
        if (publicString === "true") {
          return true;
        }
        return false;
      })();
      await this.prisma.$transaction([
        this.prisma.resource.create({
          data: {
            uploader_id: uid,
            name: fileName,
            description: resourceDTO.description,
            mime_type: file.mimeType,
            size: file.size,
            type: resourceType,
            public: resourcePublic,
          },
        }),
      ]);
      const fileURL = `/resource/${resourceType}/${uid}/${fileName}`;
      return WebResult.success(fileURL, "上传成功");
    } catch (e) {
      console.error(e);
      if (fs.existsSync(filePath)) {
        fs.rmSync(filePath);
      }
      return WebResult.failure(undefined, "上传失败，服务器内部错误");
    }
  }

  // 获取资源文件
  @Throttle({ default: { limit: 5, ttl: 1000 } }) // 每秒最多5个调用
  @Get(["/:type/:name", "/:type/:uid/:name"])
  async getResource(
    @Request() request: FastifyRequest,
    @Response() response: FastifyReply,
    @Param("uid") uid: string,
    @Param("type") resourceType: ResourceType,
    @Param("name") fileName: string,
  ) {
    if (!this.resourceService.isResourceType(resourceType)) {
      response.status(404).send("Not Found");
      return;
    }

    const whereCondition = {
      name: fileName,
      type: resourceType,
    };

    // 兼容旧的存储方式
    // 纯纯的 public 文件，置于 resource/${type} 目录下，无有信息存储
    if (!uid) {
      const resourceDirectoryPath =
        this.resourceService.getResourceDirectoryPathByType(resourceType);
      const publicFilePath = `${resourceDirectoryPath}/${fileName}`;
      if (fs.existsSync(publicFilePath)) {
        const file = fs.readFileSync(publicFilePath);
        const mimeType = this.resourceService.getMIMETypeByFileName(fileName);
        response.header("Content-Type", mimeType).send(file);
        return;
      }
    }
    // 有 uid，添加 uid 为查询条件
    else {
      const uploaderID = parseInt(uid);
      if (isNaN(uploaderID)) {
        response.status(404).send("Not Found");
        return;
      }
      whereCondition["uploader_id"] = uploaderID;
    }

    const [resource] = await this.prisma.resource.findMany({
      where: whereCondition,
      take: 1,
    });
    if (!resource) {
      response.status(404).send("Not Found");
      return;
    }

    // 判断是否登录
    // 所请求资源是登录用户上传的
    // 允许访问
    const loginInfo = await this.userTokenService.isLogin(request);
    if (loginInfo.status === "login" && resource.uploader_id === loginInfo.userID) {
      resource.public = true;
    }

    // 资源未公开
    if (!resource.public) {
      response.status(404).send("Not Found");
      return;
    }
    const filePath = this.resourceService.getResourceSavePath(
      resource.uploader_id,
      fileName,
      resourceType,
    );
    // 资源文件已不存在
    if (!fs.existsSync(filePath)) {
      await this.prisma.resource.delete({
        where: {
          id: resource.id,
        },
      });
      response.status(404).send("Not Found");
      return;
    }
    const file = fs.readFileSync(filePath);
    response.header("Content-Type", resource.mime_type).send(file);
  }

  @Throttle({ default: { limit: 5, ttl: 1000 } }) // 每秒最多5个调用
  @UseInterceptors(LoginInterceptor)
  @Get(["/self/:type/:name", "/:type/self/:name"])
  async getSelfResource(
    @Request() request: FastifyRequest,
    @Response() response: FastifyReply,
    @Param("type") resourceType: ResourceType,
    @Param("name") fileName: string,
  ) {
    const uid = this.userTokenService.getUserIDFromRequest(request);
    await this.getResource(request, response, uid?.toString(), resourceType, fileName);
  }

  @UseInterceptors(LoginInterceptor)
  @Post(["/chunk/start", "/chunk/code"])
  async getUploadResourceChunkCode(
    @Request() request: FastifyRequest,
    @Body() resourcChunkCodeDTO: ResourceChunkCodeDTO,
  ) {
    const uid = this.userTokenService.getUserIDFromRequest(request);
    const fileName = resourcChunkCodeDTO.name;
    const mimeType = this.resourceService.getMIMETypeByFileName(fileName);
    const resourceType =
      this.resourceService.getResourceTypeByMIMEType(mimeType);
    const filePath = this.resourceService.getResourceSavePath(
      uid,
      fileName,
      resourceType,
    );
    if (fs.existsSync(filePath)) {
      return WebResult.failure(undefined, "已存在同名文件");
    }
    const resourcePublic = (function() {
      if (!resourcChunkCodeDTO.public) {
        return false;
      }
      if (typeof resourcChunkCodeDTO.public === "boolean") {
        return resourcChunkCodeDTO.public;
      }
      const publicString = resourcChunkCodeDTO.public.toLowerCase()
      if (publicString === "true") {
        return true;
      }
      return false;
    })();
    resourcChunkCodeDTO.public = undefined;
    const code = this.resourceService.startUploadChunk({
      ...resourcChunkCodeDTO,
      uploaderID: uid,
      path: filePath,
      mimeType,
      size: 0,
      resourceType,
      public: resourcePublic,
    });
    return WebResult.success(code);
  }

  @UseInterceptors(LoginInterceptor)
  @Post("/chunk/end")
  async endUploadResourceChunk(@Body() endDTO: ResourceChunkEndDTO) {
    const info = await this.resourceService.endUploadChunk(endDTO.code);
    if (info) {
      await this.prisma.resource.create({
        data: {
          uploader_id: info.uploaderID,
          name: info.name,
          description: info.description,
          mime_type: info.mimeType,
          size: info.size,
          type: info.resourceType,
          public: info.public,
        },
      });
      return WebResult.success(undefined, "上传完成");
    }
    return WebResult.failure(undefined, "code错误");
  }

  @UseInterceptors(LoginInterceptor)
  @Post(["/chunk", "/upload/chunk"])
  @FormDataRequest({
    storage: FileSystemStoredFile,
    fileSystemStoragePath: CHUNK_CACHE_PATH,
  })
  async uploadResourceChunk(@Body() chunkDTO: ResourceChunkDTO) {
    const chunkFile = chunkDTO.chunk;
    const chunkBuffer = fs.readFileSync(chunkFile.path);
    const ok = await this.resourceService.saveChunk(chunkDTO.code, chunkBuffer);
    if (ok) {
      return WebResult.success(undefined, "chunk上传成功");
    }
    return WebResult.failure(undefined, "chunk上传失败");
  }

  @UseInterceptors(LoginInterceptor)
  @Post([
    "/upload/base64/image",
    "/upload/base64/img",
    "/upload/image/base64",
    "/upload/img/base64",
  ])
  async uploadBase64Image(
    @Request() request: FastifyRequest,
    @Body() imageBase64DTO: ImageBase64DTO,
  ) {
    const result = /data:image\/([^;]*);base64,/.exec(imageBase64DTO.data);
    if (!result) {
      return WebResult.failure(undefined, "format is wrong");
    }

    // 去掉 data:image/(png/jpeg/gif/...);base64
    const data = imageBase64DTO.data.replace(result[0], "");
    const format = result[1];
    const fileName = md5(data) + "." + format;
    const mimeType = `image/${format}`;
    const picBuffer = Buffer.from(data, "base64");

    const file: any = {
      originalName: fileName,
      mimetype: mimeType,
      mimeType,
      extension: format,
      buffer: picBuffer,
      size: picBuffer.length,
    };
    return await this.uploadResource(request, {
      name: fileName,
      public: true,
      file,
    });
  }

  @UseInterceptors(LoginInterceptor)
  @Post(["/upload/image", "/upload/img"])
  @FormDataRequest()
  async uploadImage(
    @Request() request: FastifyRequest,
    @Body() imgDTO: ImageDTO,
  ) {
    const img = imgDTO.img;
    if (!img) {
      return WebResult.failure(undefined, "图片不能为空");
    }

    const format = img.extension;
    const fileName = md5(img.buffer) + "." + format;
    return await this.uploadResource(request, {
      name: fileName,
      file: img,
      public: true,
    });
  }
}
